람다 + 스트림 (비교정렬, filter, toArray())

NOTE

개요 자바 스트림·람다를 코딩테스트에서 쓰기 위한 4대 주제 정리: ① 정렬(Comparable/Comparator) ② toArray() ③ flatMap 평탄화 ④ filter/Predicate.

📌 개념

1. compare 활용과 오름차순/내림차순

정렬을 구현하는 방법은 두 가지 — **클래스에 기본 정렬 기준을 심는 Comparable**과 그때그때 기준을 부여하는 Comparator.

A. 클래스 내부에서 Comparable 오버라이드 객체가 스스로의 ‘기본 정렬’을 정한다. compareTo를 오버라이드.

  • 오름차순: this - other (작은 것이 앞)
  • 내림차순: other - this (큰 것이 앞)
  • 직접 빼기는 정수 오버플로우 위험 → Integer.compare()가 안전.
class Song implements Comparable<Song> {
    int playCount;
 
    @Override
    public int compareTo(Song other) {
        // [오름차순] Integer.compare(this.playCount, other.playCount);
        // [내림차순] 남의 것에서 내 것을 뺌 (큰 수 -> 작은 수)
        return Integer.compare(other.playCount, this.playCount);
    }
}

B. 스트림에서 Comparator 사용 기본 정렬이 없거나 다른 기준으로 정렬하고 싶을 때.

  • 오름차순: .sorted(Comparator.comparing(Song::getPlayCount))
  • 내림차순: .reversed() 붙이기
  • 다중 조건: .thenComparing()
List<Song> songs = // ...
songs.stream()
     // 1. 재생수 내림차순
     .sorted(Comparator.comparing(Song::getPlayCount).reversed()
     // 2. 재생수가 같다면 인덱스 오름차순
     .thenComparing(Song::getIndex))
     .collect(Collectors.toList());

2. toArray() 완벽 가이드

스트림 끝에서 요소를 배열로 반환. 객체 배열과 원시 타입 배열의 사용법이 다르다.

A. 객체 배열 (String[], Integer[]) — 생성자 참조(클래스명[]::new)를 넘긴다.

List<String> list = Arrays.asList("A", "B", "C");
// Stream<String> -> String[]
String[] strArray = list.stream().toArray(String[]::new);

B. 원시 타입 배열 (int[], long[], double[])Stream<Integer>에서 바로 .toArray()하면 Object[]/Integer[]가 나온다. 순수 int[]반드시 mapToInt()IntStream 변환 후 .toArray().

List<Integer> intList = Arrays.asList(1, 2, 3);
 
// 에러! Object[]를 반환하므로 int[]에 담을 수 없음
// int[] wrongArr = intList.stream().toArray();
 
// 정답! mapToInt로 IntStream 변환 후 toArray() (파라미터 불필요)
int[] rightArr = intList.stream()
                        .mapToInt(Integer::intValue) // 혹은 i -> i
                        .toArray();

3. flatMap 심층 분석 (평탄화)

flatMap = Flatten(평탄화) + Map(변환). 중첩 구조를 한 겹 벗겨 하나로 합친다.

  • 비유: map은 상자들의 포장지만 바꾼다(여전히 ‘상자들’). flatMap은 상자를 모두 뜯어 ‘과일’만 하나의 바구니에 쏟는다.
List<List<String>> nestedList = Arrays.asList(
    Arrays.asList("A", "B"),
    Arrays.asList("C", "D")
);
 
// 1. map: Stream<List<String>> (리스트 안에 리스트 유지)
nestedList.stream()
          .map(list -> list);
          // 결과: [["A", "B"], ["C", "D"]]
 
// 2. flatMap: Stream<String> (알맹이만 하나로 합침)
nestedList.stream()
          .flatMap(list -> list.stream()); // 내부 리스트를 스트림으로 만들어 평탄화
          // 결과: ["A", "B", "C", "D"]

장르별 Top 2 곡 리스트 여러 개를 하나의 결과 배열로 합칠 때처럼, 중첩 컬렉션을 펴야 할 때 유용.

4. filterPredicate

filter는 조건에 맞는 요소만 걸러내며, 조건식으로 Predicate(참/거짓 반환 함수형 인터페이스)를 받는다.

A. 람다식 인라인 (가장 일반적)

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
 
numbers.stream()
       .filter(n -> n % 2 == 0)          // 짝수 판별
       .forEach(System.out::println);    // 2, 4

B. Predicate 변수 선언 (재사용·조합) 조건이 복잡하거나 여러 곳에서 재사용할 때. and(), or(), negate()로 조합.

Predicate<Song> isPopular = song -> song.playCount > 1000;
Predicate<Song> isClassic = song -> song.genre.equals("classic");
 
List<Song> result = songs.stream()
    // 조건 조합: "재생수 1000 초과 AND 클래식 장르"
    .filter(isPopular.and(isClassic))
    // "재생수가 1000 이하인 곡" → .filter(isPopular.negate())
    .collect(Collectors.toList());

명시적 선언 방식은 코드가 영어 문장처럼 읽혀 가독성이 크게 올라간다.


🔗 참고